iT邦幫忙

2026 iThome 鐵人賽

DAY 2
0
自我挑戰組

從多數決到可信投票:30 天打造 TACT 大語言模型推理聚合方法系列 第 2

Day 2|Self-Consistency 只有一行?我在平手規則踩過的坑

  • 分享至 

  • xImage
  •  

上一篇把問題攤開後,這回來寫最小版 Self-Consistency。多數決那一行很短,旁邊那些看似無聊的小事才會咬人。

我以前真的寫過這種一行版本:

Counter(answers).most_common(1)[0][0]

它會回傳最高票,看起來完全沒問題。麻煩是平手時,它直接拿最先出現的答案,整個過程安安靜靜,連警告都沒有。

我就被這件事坑過。當時模擬器把正確答案固定編成 0,argmax 又偏向最小索引,平手題於是白拿了一大段準確率。那份漂亮成績跟演算法無關。後來我把答案擷取、字串整理和平手處理拆開,才比較容易看出哪一步出錯。

別從整段推理硬猜答案

我會要求模型把最後答案寫成:

FINAL: A

推理寫多長都沒關係,聚合器只讀最後一個 FINAL。模型沒照格式回答時,我寧可讓程式報錯,也不想把整段推理誤當答案。

import re

FINAL_RE = re.compile(
    r'(?im)^\s*FINAL\s*:\s*(.+?)\s*$'
)

def normalize_answer(raw: str) -> str:
    matches = FINAL_RE.findall(raw)

    if not matches:
        raise ValueError('找不到 FINAL: ...')

    answer = matches[-1]
    answer = re.sub(r'\s+', ' ', answer)
    return answer.strip()

這裡只整理空白,沒有把所有文字一律轉成小寫。數學答案尤其不能靠幾個 replace 硬湊:0.5、1/2 和 LaTeX 分數可能是同一個值,我會留到答案等價那篇再處理。

平手就老實說平手

from collections import Counter
from dataclasses import dataclass

@dataclass(frozen=True)
class VoteResult:
    answer: str | None
    counts: dict[str, int]
    tied_answers: tuple[str, ...]

def plurality_vote(raw_outputs: list[str]) -> VoteResult:
    if not raw_outputs:
        raise ValueError('至少需要一筆模型輸出')

    answers = [normalize_answer(x) for x in raw_outputs]
    counts = Counter(answers)
    top_count = max(counts.values())

    winners = tuple(
        sorted(
            answer
            for answer, count in counts.items()
            if count == top_count
        )
    )

    return VoteResult(
        answer=winners[0] if len(winners) == 1 else None,
        counts=dict(counts),
        tied_answers=winners if len(winners) > 1 else (),
    )

我讓平手時的 answer 回傳 None。真的非得選一個答案,可以用固定亂數種子抽籤,也可以再向模型多取幾條軌跡。選哪種都行,但規則要先寫進實驗設定,平手率也要留下來。

丟 16 條假資料進去看看

outputs = (
    ['推理略...\nFINAL: A'] * 9
    + ['推理略...\nFINAL: B'] * 5
    + ['推理略...\nFINAL: C'] * 2
)

result = plurality_vote(outputs)

print(result.answer)
print(result.counts)
print(result.tied_answers)

輸出會是:

A
{'A': 9, 'B': 5, 'C': 2}
()

投票器確認沒問題後,再把模型呼叫包進來:

from collections.abc import Callable

def self_consistency(
    question: str,
    ask: Callable[[str], str],
    k: int = 16,
) -> VoteResult:
    outputs = [ask(question) for _ in range(k)]
    return plurality_vote(outputs)

ask 可以接任何 LLM API。我習慣把取樣和聚合分開,換模型或換 API 時,比較不會連投票器一起改壞。

16 條軌跡的冗餘壓力測試

圖:當多條推理軌跡高度相似時,16 票不等於 16 份獨立證據。群內相關越高,有效票數越快飽和;這也是後續需要另外處理重複軌跡的原因。

我現在會多存幾個欄位

以前我只存最後答案,出問題時幾乎沒東西可以查。現在至少會留原始輸出、正規化答案、各答案票數、平手狀態、取樣參數和亂數種子。準確率突然變動時,才能查清楚是聚合器動了手腳,或只是這批樣本剛好不同。

這支程式跑通後,下一篇就替每條軌跡加上信心分數。正向加權只要改幾行,卻也正是高信心錯誤開始搶走票數的地方。

論文預印本:TACT: Trust-Anchored Confidence Tempering for Self-Consistency Voting in Large Language Models


上一篇
Day 1|如果模型會投票,每一票真的都該一樣重嗎?
系列文
從多數決到可信投票:30 天打造 TACT 大語言模型推理聚合方法2
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言